import os, math, random, datetime
from collections import deque, namedtuple
from itertools import count

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from tqdm import tqdm

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F

plt.rcParams['font.family'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
sns.set(style='whitegrid', font='SimHei')

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# -------------------- 超参（加速配置） --------------------
BATCH_SIZE   = 64
GAMMA        = 0.98
EPS_START    = 0.95
EPS_END      = 0.01
EPS_DECAY    = 800
TAU          = 0.005
LR           = 1e-3
N_EPISODES   = 50          # ← 降至 300 即可收敛
MAX_STEPS    = 180
CAPACITY     = 10000
ACTION_SPACE = np.arange(0, 5001, 100)          # 0,100,...,5000
N_ACTIONS    = len(ACTION_SPACE)
COMPUTE_FREQ = 50           # ← 指标稀疏化
# -------------------- 数据 --------------------
def load_data():
    df = pd.read_csv('婴儿车库存管理完整数据.csv')
    df['日期'] = pd.to_datetime(df['日期'])
    df = df.sort_values('日期').reset_index(drop=True)
    unit_h = df['单位持有成本(元/辆/天)'].iloc[0]
    unit_s = df['单位缺货成本(元/辆)'].iloc[0]
    unit_p = df['单位采购成本(元/辆)'].iloc[0]
    lead   = int(df['补货提前期(天)'].iloc[0])
    max_inv= int(df['最大库存容量(辆)'].iloc[0])
    safety = int(df['安全库存(辆)'].iloc[0])
    df['需求'] = np.where(df['数据类型']=='历史数据', df['实际销量(辆)'], df['预测出口量(辆)'])
    demand = df['需求'].values
    return df, demand, unit_h, unit_s, unit_p, lead, max_inv, safety

DATA, DEMAND, UNIT_H, UNIT_S, UNIT_P, LEAD, MAX_INV, SAFETY = load_data()
# -------------------- 环境 --------------------
class InventoryEnv:
    def __init__(self):
        self.demand = DEMAND
        self.max_steps = MAX_STEPS
        self.lead = LEAD
        self.max_inv = MAX_INV
        self.unit_h = UNIT_H
        self.unit_s = UNIT_S
        self.unit_p = UNIT_P

    def reset(self):
        self.day = 0
        self.inv  = DATA.iloc[0]['期末库存(辆)']
        self.pipeline = [DATA.iloc[i]['在途库存(辆)'] if i<len(DATA) else 0
                         for i in range(self.lead)]
        self.arrive = 0
        self.cost_log = []
        self.stock_log = []
        self.inv_log = []
        self.repl_log = []
        return self._state()

    def _state(self):
        d = self.demand[self.day] if self.day < len(self.demand) else self.demand[-1]
        return np.array([self.inv / self.max_inv,
                         sum(self.pipeline) / self.max_inv,
                         d / self.max_inv,
                         self.day / 365,
                         np.sin(2 * np.pi * self.day / 365),
                         np.cos(2 * np.pi * self.day / 365)],
                        dtype=np.float32)

    def step(self, action):
        # ===== 0. 硬下限：若库存<80%SS，强制补到SS =====
        if self.inv < SAFETY * 1.0:
            forced_replen = max(SAFETY - self.inv, 0)
            action = np.argmin(np.abs(ACTION_SPACE - forced_replen))
        replenish = ACTION_SPACE[action]

        # ===== 1. 到货入库 =====
        self.arrive = self.pipeline.pop(0)
        self.pipeline.append(replenish)
        self.inv += self.arrive
        self.inv = min(self.inv, self.max_inv)

        # ===== 2. 需求出货 =====
        d = self.demand[self.day]
        stockout = max(0, d - self.inv)
        self.inv = max(0, self.inv - d)

        # ===== 3. 成本计算 =====
        hold_cost = self.inv * self.unit_h
        stock_cost = stockout * 3000
        purchase_cost = replenish * self.unit_p + (500 if replenish > 0 else 0)
        total_cost = hold_cost + stock_cost + purchase_cost

        # ===== 4. 惩罚 & 奖励 =====
        stockout_rate = stockout / max(d, 1)
        penalty = (4000 * max(0, stockout_rate - 0.01) +
                   600 * max(0, (self.inv / self.max_inv) - 0.8))
        reward = - (total_cost + penalty)

        # ===== 5. 记录 & 返回 =====
        self.cost_log.append(total_cost)
        self.stock_log.append(stockout)
        self.inv_log.append(self.inv)
        self.repl_log.append(replenish)

        self.day += 1
        done = self.day >= self.max_steps
        return self._state(), reward, done, {
            'inv': self.inv,
            'replenish': replenish,
            'stockout': stockout,
            'total_cost': total_cost,
            'hold': hold_cost,
            'stock': stock_cost,
            'purchase': purchase_cost
        }
# -------------------- 网络 --------------------
class DQN(nn.Module):
    def __init__(self, n_actions):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(6, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, n_actions)
        )
    def forward(self, x):
        return self.net(x)
# -------------------- Buffer --------------------
Transition = namedtuple('Transition', ('state', 'action', 'next_state', 'reward'))
class ReplayBuffer:
    def __init__(self, capacity):
        self.memory = deque([], maxlen=capacity)
    def push(self, *args):
        self.memory.append(Transition(*args))
    def sample(self, batch_size):
        return random.sample(self.memory, batch_size)
    def __len__(self):
        return len(self.memory)
# -------------------- Agent --------------------
class DQNAgent:
    def __init__(self, n_actions):
        self.n_actions = n_actions
        self.policy_net = DQN(n_actions).to(device)
        self.target_net = DQN(n_actions).to(device)
        self.target_net.load_state_dict(self.policy_net.state_dict())
        self.optimizer = optim.Adam(self.policy_net.parameters(), lr=LR)
        self.memory = ReplayBuffer(CAPACITY)
        self.steps_done = 0
    def select_action(self, state, train=True):
        eps = EPS_END + (EPS_START - EPS_END) * math.exp(-1. * self.steps_done / EPS_DECAY)
        self.steps_done += 1
        if train and random.random() < eps:
            return torch.tensor([[random.randrange(self.n_actions)]], device=device, dtype=torch.long)
        else:
            with torch.no_grad():
                return self.policy_net(torch.tensor(state, device=device).unsqueeze(0)).max(1)[1].view(1, 1)
    def optimize(self):
        if len(self.memory) < BATCH_SIZE:
            return
        transitions = self.memory.sample(BATCH_SIZE)
        batch = Transition(*zip(*transitions))
        non_final_mask = torch.tensor(tuple(map(lambda s: s is not None, batch.next_state)),
                                      device=device, dtype=torch.bool)
        non_final_next_states = torch.cat([torch.tensor(s, device=device).unsqueeze(0)
                                           for s in batch.next_state if s is not None])
        state_batch = torch.cat([torch.tensor(s, device=device).unsqueeze(0) for s in batch.state])
        action_batch = torch.cat(batch.action)
        reward_batch = torch.cat(batch.reward)

        q_values = self.policy_net(state_batch).gather(1, action_batch)
        next_q_values = torch.zeros(BATCH_SIZE, device=device)
        next_q_values[non_final_mask] = self.target_net(non_final_next_states).max(1)[0].detach()
        expected = (next_q_values * GAMMA) + reward_batch
        loss = F.smooth_l1_loss(q_values, expected.unsqueeze(1))
        self.optimizer.zero_grad(); loss.backward(); self.optimizer.step()

    def update_target(self):
        self.target_net.load_state_dict(self.policy_net.state_dict())
# -------------------- 指标计算 --------------------
def compute_metrics(env):
    log = env.cost_log
    stock = env.stock_log
    inv   = env.inv_log
    repl  = env.repl_log
    demand = env.demand[:env.max_steps]

    avg_cost = np.mean(log)
    stockout_rate = np.mean(np.array(stock) > 0)
    service_level = 1 - stockout_rate
    turnover = np.sum(demand) / np.mean(inv)
    cost_var = np.var(log)
    cvar5 = np.mean(sorted(log)[-int(0.05 * len(log)):])
    gini = 2 * np.sum(np.arange(1, len(log) + 1) * np.sort(log)) / (len(log) * np.sum(log)) - 1
    bullwhip = np.var(repl) / np.var(demand)
    ss_util = np.mean(inv) / SAFETY
    return {
        'avg_cost': avg_cost,
        'stockout_rate': stockout_rate,
        'service_level': service_level,
        'inventory_turnover': turnover,
        'cost_var': cost_var,
        'cvar_5': cvar5,
        'gini': gini,
        'bullwhip': bullwhip,
        'ss_util': ss_util
    }
# -------------------- 训练 --------------------
def train_v2_speed():
    env = InventoryEnv()
    agent = DQNAgent(N_ACTIONS)
    metrics_list = []
    cost_hist = []
    for ep in tqdm(range(N_EPISODES), desc='DQN-v2-Speed Training'):
        state = env.reset()
        total_cost = 0
        for t in count():
            action = agent.select_action(state, train=True)
            next_s, reward, done, info = env.step(action.item())
            total_cost += info['total_cost']
            agent.memory.push(state, action, next_s, torch.tensor([reward], device=device))
            state = next_s
            agent.optimize()
            if done:
                break
        cost_hist.append(total_cost / env.max_steps)
        # 每 50 回合计算指标（加速关键）
        if ep % COMPUTE_FREQ == 0 or ep == N_EPISODES - 1:
            met = compute_metrics(env)
            met['episode'] = ep
            metrics_list.append(met)
        if ep % 10 == 0:
            agent.update_target()
    metrics_df = pd.DataFrame(metrics_list)
    metrics_df.to_csv('results/metrics_speed.csv', index=False)
    return agent, metrics_df
# -------------------- 评估 & 基准 --------------------
def evaluate(agent):
    env = InventoryEnv()
    state = env.reset()
    log = []
    with torch.no_grad():
        for t in range(env.max_steps):
            action = agent.select_action(state, train=False)
            state, _, done, info = env.step(action.item())
            info['需求'] = env.demand[t]
            log.append(info)
            if done:
                break
    return pd.DataFrame(log)

def baseline():
    env = InventoryEnv()
    state = env.reset()
    log = []
    for t in range(env.max_steps):
        inv = env.inv
        to_order = max(SAFETY - inv, 0)
        action = np.argmin(np.abs(ACTION_SPACE - to_order))
        state, _, done, info = env.step(action)
        info['需求'] = env.demand[t]
        log.append(info)
        if done:
            break
    return pd.DataFrame(log)
# -------------------- 绘图 --------------------
# def plot_all(dqn_df, base_df, metrics_df):
#     os.makedirs('charts', exist_ok=True)

#     # 统一字体：Arial Unicode MS 含 ¥ 符号
#     plt.rcParams['font.family'] = ['Arial Unicode MS', 'SimHei']
#     plt.rcParams['axes.unicode_minus'] = False

#     # 1. 收敛曲线
#     plt.figure()
#     plt.plot(metrics_df['episode'], metrics_df['avg_cost'])
#     plt.title('Average Daily Cost');
#     plt.xlabel('Episode');
#     plt.ylabel('Cost (¥)')
#     plt.grid();
#     plt.savefig('charts/avg_cost_convergence.png', dpi=300);
#     plt.close()

#     # 2. 缺货率
#     plt.figure()
#     plt.plot(metrics_df['episode'], metrics_df['stockout_rate'] * 100, color='C3')
#     plt.title('Stock-out Rate');
#     plt.xlabel('Episode');
#     plt.ylabel('%');
#     plt.grid()
#     plt.savefig('charts/stockout_rate_curve.png', dpi=300);
#     plt.close()

#     # 3. 箱线图
#     plt.figure()
#     data = [base_df['total_cost'], dqn_df['total_cost']]
#     sns.boxplot(data=data)
#     plt.xticks([0, 1], ['Baseline', 'DQN'])  # ← 用 xticks 代替 labels
#     plt.title('Daily Cost Distribution');
#     plt.ylabel('Cost (¥)')
#     plt.savefig('charts/cost_boxplot.png', dpi=300);
#     plt.close()

#     # 4. 累积成本
#     plt.figure()
#     plt.plot(dqn_df['total_cost'].cumsum(), label='DQN')
#     plt.plot(base_df['total_cost'].cumsum(), label='Baseline')
#     plt.title('Cumulative Cost');
#     plt.legend();
#     plt.grid()
#     plt.savefig('charts/cumulative_cost.png', dpi=300);
#     plt.close()

#     # 5. 库存-补货
#     fig, ax1 = plt.subplots()
#     ax2 = ax1.twinx()
#     ax1.plot(dqn_df['inv'], 'b-', label='Inventory')
#     ax2.plot(dqn_df['replenish'], 'r--', label='Replenish')
#     ax1.set_ylabel('Inventory');
#     ax2.set_ylabel('Replenish')
#     plt.title('Inventory & Replenish');
#     plt.savefig('charts/inv_replenish.png', dpi=300);
#     plt.close()

#     # 6. 成本饼图
#     plt.figure()
#     cost_comp = [dqn_df['hold'].sum(), dqn_df['stock'].sum(), dqn_df['purchase'].sum()]
#     plt.pie(cost_comp, labels=['Holding', 'Stock-out', 'Purchase'], autopct='%1.1f%%')
#     plt.title('Cost Composition');
#     plt.savefig('charts/cost_pie.png', dpi=300);
#     plt.close()

#     # 7. Gini
#     plt.figure()
#     plt.plot(metrics_df['episode'], metrics_df['gini'])
#     plt.title('Gini Coefficient');
#     plt.grid()
#     plt.savefig('charts/gini_curve.png', dpi=300);
#     plt.close()

#     # 8. Bullwhip
#     plt.figure()
#     plt.plot(metrics_df['episode'], metrics_df['bullwhip'])
#     plt.title('Bullwhip Effect');
#     plt.grid()
#     plt.savefig('charts/bullwhip_curve.png', dpi=300);
#     plt.close()

def plot_all(dqn_df, base_df, metrics_df):
    os.makedirs('charts', exist_ok=True)
    #     # 统一字体：Arial Unicode MS 含 ¥ 符号
    plt.rcParams['font.family'] = ['Arial Unicode MS', 'SimHei']
    plt.rcParams['axes.unicode_minus'] = False
    # 1. 平均日成本收敛曲线
    plt.figure()
    plt.plot(metrics_df['episode'], metrics_df['avg_cost'], color='steelblue', linewidth=2, label='DQN')
    plt.title('图 4-1 平均日成本收敛曲线', pad=10)
    plt.xlabel('训练回合')
    plt.ylabel('平均日成本 (¥)')
    plt.grid(True, linestyle='--', alpha=0.5)
    plt.tight_layout(pad=2)
    plt.savefig('charts/图4-1_平均日成本收敛曲线.png', dpi=300, bbox_inches='tight')
    plt.close()

    # 2. 缺货率曲线
    plt.figure()
    plt.plot(metrics_df['episode'], metrics_df['stockout_rate'] * 100, color='C3', linewidth=2, label='DQN')
    plt.title('图 4-2 缺货率收敛曲线', pad=10)
    plt.xlabel('训练回合')
    plt.ylabel('缺货率 (%)')
    plt.grid(True, linestyle='--', alpha=0.5)
    plt.tight_layout(pad=2)
    plt.savefig('charts/图4-2_缺货率收敛曲线.png', dpi=300, bbox_inches='tight')
    plt.close()

    # 3. 日成本分布箱线图
    plt.figure()
    sns.boxplot(data=[base_df['total_cost'], dqn_df['total_cost']], palette=['gray', 'steelblue'])
    plt.xticks([0, 1], ['基准策略', 'DQN策略'], fontsize=14)
    plt.ylabel('日成本 (¥)', fontsize=16)
    plt.title('图 4-3 日成本分布对比', pad=10)
    plt.grid(True, linestyle='--', alpha=0.5)
    plt.tight_layout(pad=2)
    plt.savefig('charts/图4-3_日成本分布对比.png', dpi=300, bbox_inches='tight')
    plt.close()


    # 4. 累计成本对比
    plt.figure()
    plt.plot(dqn_df['total_cost'].cumsum(), label='DQN', color='steelblue', linewidth=2)
    plt.plot(base_df['total_cost'].cumsum(), label='基准策略', color='gray', linewidth=2)
    plt.title('图 4-4 累计成本对比', pad=10)
    plt.xlabel('天数')
    plt.ylabel('累计成本 (¥)')
    plt.legend(frameon=False)
    plt.grid(True, linestyle='--', alpha=0.5)
    plt.tight_layout(pad=2)
    plt.savefig('charts/图4-4_累计成本对比.png', dpi=300, bbox_inches='tight')
    plt.close()

    # 5. 库存与补货双轴图
    fig, ax1 = plt.subplots(figsize=(10, 6))
    ax2 = ax1.twinx()
    ax1.plot(dqn_df['inv'], color='steelblue', linewidth=2, label='库存')
    ax2.plot(dqn_df['replenish'], color='orangered', linewidth=2, linestyle='--', label='补货量')
    ax1.set_ylabel('库存 (辆)', fontsize=16)
    # ax2.set_ylabel('补货量 (辆)', fontsize=16, rotation=270, labelpad=15, va='top')
    ax2.set_ylabel('补货量 (辆)', rotation=270, labelpad=15, va='top')

    ax1.set_xlabel('天数')
    plt.title('图 4-5 库存与补货对比', pad=10)
    fig.legend(loc='upper right', bbox_to_anchor=(0.92, 0.92), frameon=False)
    plt.grid(True, linestyle='--', alpha=0.5)
    plt.tight_layout(pad=2)
    plt.savefig('charts/图4-5_库存与补货对比.png', dpi=300, bbox_inches='tight')
    plt.close()

    # 6. 成本构成饼图
    plt.figure()
    cost_comp = [dqn_df['hold'].sum(), dqn_df['stock'].sum(), dqn_df['purchase'].sum()]
    plt.pie(cost_comp, labels=['持有成本', '缺货成本', '采购成本'], autopct='%1.1f%%', startangle=90)
    plt.title('图 4-6 成本构成', pad=10)
    plt.savefig('charts/图4-6_成本构成.png', dpi=300, bbox_inches='tight')
    plt.close()

    # 7. 基尼系数
    plt.figure()
    plt.plot(metrics_df['episode'], metrics_df['gini'], color='darkgreen', linewidth=2)
    plt.title('图 4-7 基尼系数收敛曲线', pad=10)
    plt.xlabel('训练回合')
    plt.ylabel('基尼系数')
    plt.grid(True, linestyle='--', alpha=0.5)
    plt.tight_layout(pad=2)
    plt.savefig('charts/图4-7_基尼系数收敛曲线.png', dpi=300, bbox_inches='tight')
    plt.close()

    # 8. 牛鞭效应
    plt.figure()
    plt.plot(metrics_df['episode'], metrics_df['bullwhip'], color='purple', linewidth=2)
    plt.title('图 4-8 牛鞭效应收敛曲线', pad=10)
    plt.xlabel('训练回合')
    plt.ylabel('牛鞭比率')
    plt.grid(True, linestyle='--', alpha=0.5)
    plt.tight_layout(pad=2)
    plt.savefig('charts/图4-8_牛鞭效应收敛曲线.png', dpi=300, bbox_inches='tight')
    plt.close()


    # ============= 新增 4-10 ~ 4-14 （30 天滚动证据） =============
    os.makedirs('charts', exist_ok=True)
    df_30 = dqn_df.head(30).copy()
    df_30['日期'] = pd.date_range(start='2025-09-06', periods=30, freq='D')

    # 4-10 30 天滚动补货计划
    plt.figure(figsize=(12, 5))
    plt.bar(df_30['日期'], df_30['replenish'], color='orangered', alpha=0.8, label='建议补货')
    plt.plot(df_30['日期'], df_30['需求'], color='steelblue', linewidth=2, label='预测需求')
    plt.axhline(y=SAFETY, color='gray', linestyle='--', label='安全库存')

    # 关键：强制每日标签
    # plt.gca().xaxis.set_major_locator(mdates.DayLocator(interval=1))  # 每 1 天
    # plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%m-%d'))  # 月-日格式
    plt.xticks(rotation=45, fontsize=12)

    plt.title('图 4-10 30 天滚动补货计划', fontsize=16)
    plt.xlabel('日期')
    plt.ylabel('数量（辆）')
    plt.legend(frameon=False)
    plt.grid(True, linestyle='--', alpha=0.5)
    plt.tight_layout(pad=2)
    plt.savefig('charts/图4-10_30天滚动补货计划.png', dpi=300, bbox_inches='tight')
    plt.close()

    # 4-11 30 天累计缺货对比
    plt.figure(figsize=(10, 5))
    plt.plot(df_30['日期'], df_30['stockout'].cumsum(), color='red', linewidth=2, label='DQN累计缺货')
    plt.plot(df_30['日期'], base_df.head(30)['stockout'].cumsum(), color='gray', linewidth=2, label='基准累计缺货')
    plt.xticks(rotation=45, fontsize=12)

    plt.title('图 4-11 30 天累计缺货对比', fontsize=16)
    plt.xlabel('日期')
    plt.ylabel('累计缺货（辆）')
    plt.legend(frameon=False)
    plt.grid(True, linestyle='--', alpha=0.5)
    plt.tight_layout(pad=2)
    plt.savefig('charts/图4-11_30天累计缺货对比.png', dpi=300, bbox_inches='tight')
    plt.close()

    # 4-12 30 天累计成本对比
    plt.figure(figsize=(10, 5))
    plt.plot(df_30['日期'], df_30['total_cost'].cumsum(), color='steelblue', linewidth=2, label='DQN累计成本')
    plt.plot(df_30['日期'], base_df.head(30)['total_cost'].cumsum(), color='gray', linewidth=2, label='基准累计成本')
    plt.xticks(rotation=45, fontsize=12)

    plt.title('图 4-12 30 天累计成本对比', fontsize=16)
    plt.xlabel('日期')
    plt.ylabel('累计成本 (¥)')
    plt.legend(frameon=False)
    plt.grid(True, linestyle='--', alpha=0.5)
    plt.tight_layout(pad=2)
    plt.savefig('charts/图4-12_30天累计成本对比.png', dpi=300, bbox_inches='tight')
    plt.close()

    # 4-13 30 天库存水位 vs 安全库存
    plt.figure(figsize=(10, 5))
    plt.plot(df_30['日期'], df_30['inv'], color='steelblue', linewidth=2, label='当前库存')
    plt.axhline(y=SAFETY, color='gray', linestyle='--', label='安全库存')
    plt.fill_between(df_30['日期'], 0, SAFETY, alpha=0.2, color='red', label='安全库存以下')
    plt.xticks(rotation=45, fontsize=12)

    plt.title('图 4-13 30 天库存水位 vs 安全库存', fontsize=16)
    plt.xlabel('日期')
    plt.ylabel('库存（辆）')
    plt.legend(frameon=False)
    plt.grid(True, linestyle='--', alpha=0.5)
    plt.tight_layout(pad=2)
    plt.savefig('charts/图4-13_30天库存水位对比.png', dpi=300, bbox_inches='tight')
    plt.close()

    
# -------------------- LaTeX 表格 --------------------
def export_tables(dqn_df, base_df, metrics_df):
    os.makedirs('tables', exist_ok=True)
    final = metrics_df.iloc[-1]
    base_total = base_df['total_cost'].sum()
    dqn_total = dqn_df['total_cost'].sum()
    imp_cost = (base_total - dqn_total) / base_total * 100
    summary = pd.DataFrame({
        'Metric': ['Average Daily Cost (¥)', 'Stock-out Rate (%)', 'Service Level (%)', 'Inventory Turnover',
                   'Cost Variance', 'CVaR@5% (¥)', 'Gini Coefficient', 'Bullwhip Ratio'],
        'Baseline': [base_df['total_cost'].mean(), (base_df['stockout'] > 0).mean() * 100,
                     (1 - (base_df['stockout'] > 0).mean()) * 100,
                     base_df['需求'].sum() / base_df['inv'].mean(),
                     base_df['total_cost'].var(), np.percentile(base_df['total_cost'], 95),
                     2 * np.sum(np.arange(1, len(base_df) + 1) * np.sort(base_df['total_cost'])) / (
                             len(base_df) * base_df['total_cost'].sum()) - 1,
                     np.var(base_df['replenish']) / np.var(base_df['需求'])],
        'DQN': [final['avg_cost'], final['stockout_rate'] * 100, final['service_level'] * 100,
                final['inventory_turnover'], final['cost_var'], final['cvar_5'],
                final['gini'], final['bullwhip']],
        'Improvement': [f'{imp_cost:.1f}% ↓', f'{(final["service_level"] - (1 - (base_df["stockout"] > 0).mean())) * 100:.1f}pp ↑', '',
                        f'{(final["inventory_turnover"] - base_df["需求"].sum() / base_df["inv"].mean()) / (base_df["需求"].sum() / base_df["inv"].mean()) * 100:.1f}% ↑',
                        f'{(final["cost_var"] - base_df["total_cost"].var()) / base_df["total_cost"].var() * 100:.1f}% ↓',
                        f'{(final["cvar_5"] - np.percentile(base_df["total_cost"], 95)) / np.percentile(base_df["total_cost"], 95) * 100:.1f}% ↓',
                        f'{(final["gini"] - (2 * np.sum(np.arange(1, len(base_df) + 1) * np.sort(base_df["total_cost"])) / (len(base_df) * base_df["total_cost"].sum()) - 1)) / (2 * np.sum(np.arange(1, len(base_df) + 1) * np.sort(base_df["total_cost"])) / (len(base_df) * base_df["total_cost"].sum()) - 1) * 100:.1f}% ↓',
                        f'{(final["bullwhip"] - np.var(base_df["replenish"]) / np.var(base_df["需求"])) / (np.var(base_df["replenish"]) / np.var(base_df["需求"])) * 100:.1f}% ↓']
    })
    summary.to_csv('tables/summary.csv', index=False, encoding='utf-8-sig')
    latex = summary.to_latex(index=False, float_format='%.2f', escape=False,
                             caption='Performance comparison (300 episodes, last 50 avg)',
                             label='tab:compare')
    with open('tables/table_comparison.tex', 'w', encoding='utf-8') as f:
        f.write(latex)
    print(' LaTeX 表格已保存：tables/table_comparison.tex')
# -------------------- 补货建议 --------------------
# def export_plan(agent):
#     agent.policy_net.eval()
#     env = InventoryEnv()
#     state = env.reset()
#     plan = []
#     with torch.no_grad():
#         for t in range(env.max_steps):
#             action = agent.select_action(state, train=False)
#             state, _, done, info = env.step(action.item())
#             plan.append({
#                 '日期': (datetime.datetime(2025, 9, 6) + datetime.timedelta(days=t)).strftime('%Y-%m-%d'),
#                 '预测需求': env.demand[t],
#                 '当前库存': int(info['inv']),
#                 '在途到货': env.arrive,
#                 '建议补货': int(info['replenish']),
#                 '预计缺货': int(info['stockout'])
#             })
#             if done:
#                 break
#     pd.DataFrame(plan).to_csv('replenishment_plan_30days.csv', index=False, encoding='utf-8-sig')
#     print('补货建议已保存：replenishment_plan_30days.csv')


def export_plan(agent):
    agent.policy_net.eval()
    env = InventoryEnv()
    state = env.reset()
    plan = []

    print("=== 补货计划调试信息 ===")
    print(f"初始库存: {env.inv}, 安全库存: {SAFETY}, 强制补货阈值: {SAFETY * 0.8}")
    print(f"补货提前期: {env.lead}天, 最大库存容量: {env.max_inv}")
    print(f"动作空间: {ACTION_SPACE[:10]}...")  # 显示前10个动作

    with torch.no_grad():
        for t in range(env.max_steps):
            # 记录决策前的状态
            current_inv = env.inv
            pipeline_total = sum(env.pipeline)
            demand_today = env.demand[t] if t < len(env.demand) else env.demand[-1]

            action = agent.select_action(state, train=False)
            planned_replenish = ACTION_SPACE[action.item()]

            print(f"Day {t}: 库存={current_inv}, 管道库存={pipeline_total}, 需求={demand_today}")
            print(f"       动作索引={action.item()}, 计划补货量={planned_replenish}")

            state, _, done, info = env.step(action.item())

            # 检查强制补货是否触发
            actual_replenish = info['replenish']
            if planned_replenish != actual_replenish:
                print(f"        强制补货触发: {planned_replenish} -> {actual_replenish}")

            plan.append({
                '日期': (datetime.datetime(2025, 9, 6) + datetime.timedelta(days=t)).strftime('%Y-%m-%d'),
                '预测需求': demand_today,
                '当前库存': int(info['inv']),
                '在途到货': int(info.get('arrive', env.arrive)),  # 使用env.arrive作为备选
                '建议补货': int(actual_replenish),
                '预计缺货': int(info['stockout']),
                '管道库存总和': int(pipeline_total),  # 新增字段用于调试
                '决策前库存': int(current_inv)  # 新增字段用于调试
            })

            if done:
                break

    # 输出详细分析报告
    print("\n" + "=" * 50)
    print("=" * 50)

    df_plan = pd.DataFrame(plan)

    # 统计补货情况
    total_replenish = df_plan['建议补货'].sum()
    zero_replenish_days = (df_plan['建议补货'] == 0).sum()
    stockout_days = (df_plan['预计缺货'] > 0).sum()

    print(f"总模拟天数: {len(df_plan)}")
    print(f"总补货量: {total_replenish}")
    print(f"零补货天数: {zero_replenish_days} ({zero_replenish_days / len(df_plan) * 100:.1f}%)")
    print(f"缺货天数: {stockout_days} ({stockout_days / len(df_plan) * 100:.1f}%)")
    print(f"平均每日库存: {df_plan['当前库存'].mean():.0f}")
    print(f"平均每日缺货: {df_plan['预计缺货'].mean():.0f}")

    # 显示前10天的详细情况
    print("\n前10天详细情况:")
    print("日期        预测需求  决策前库存  建议补货  在途到货  当前库存  预计缺货")
    for i in range(min(10, len(plan))):
        p = plan[i]
        print(
            f"{p['日期']}  {p['预测需求']:4.0f}     {p['决策前库存']:4.0f}       {p['建议补货']:4.0f}      {p['在途到货']:4.0f}      {p['当前库存']:4.0f}      {p['预计缺货']:4.0f}")

    # 保存到CSV
    output_df = pd.DataFrame(plan)
    # 移除调试字段，只保留原始结构的字段
    final_output = output_df[['日期', '预测需求', '当前库存', '在途到货', '建议补货', '预计缺货']]
    final_output.to_csv('replenishment_plan_30days.csv', index=False, encoding='utf-8-sig')

    print(f'\n补货建议已保存：replenishment_plan_30days.csv')

    return final_output
# -------------------- 报告 --------------------
def report(dqn_df, base_df, metrics_df):
    final = metrics_df.iloc[-1]
    imp_cost = (base_df['total_cost'].sum() - dqn_df['total_cost'].sum()) / base_df['total_cost'].sum() * 100
    md = f"""# 婴儿车 SKU-001 DQN-v7-Speed 报告

## 摘要（300 回合，指标稀疏化）
- **平均日成本**下降 **{imp_cost:.1f}%**
- **缺货率**下降 **{(final['service_level'] - (1 - (base_df['stockout'] > 0).mean())) * 100:.1f}** 个百分点
- **库存周转**提升 **{(final['inventory_turnover'] - base_df['需求'].sum() / base_df['inv'].mean()) / (base_df['需求'].sum() / base_df['inv'].mean()) * 100:.1f}%**
- **CVaR@5%** 下降 **{(final['cvar_5'] - np.percentile(base_df['total_cost'], 95)) / np.percentile(base_df['total_cost'], 95) * 100:.1f}%**
- **Gini 系数** 下降 **{(final['gini'] - (2 * np.sum(np.arange(1, len(base_df) + 1) * np.sort(base_df['total_cost'])) / (len(base_df) * base_df['total_cost'].sum()) - 1)) / (2 * np.sum(np.arange(1, len(base_df) + 1) * np.sort(base_df['total_cost'])) / (len(base_df) * base_df['total_cost'].sum()) - 1) * 100:.1f}%**

## 1. 指标对比
见表 `tables/table_comparison.tex`

## 2. 补货建议
`replenishment_plan_30days.csv` 可直接导入 ERP

## 3. 图表
8 张高清图已保存在 `charts/`（训练后统一生成）

> 模型已就绪，替换 CSV 即可复用；提速≈5×，300 回合≈10 分钟
"""
    with open('SKU001_DQN_Report_v7.md', 'w', encoding='utf-8') as f:
        f.write(md)
    print(' 报告已保存：SKU001_DQN_Report_v7.md')
# -------------------- main --------------------

def check_model(dqn_df, base_df, demand, ss=5000):
    res = {}
    res['C̄'] = dqn_df['total_cost'].mean()
    res['C̄_base'] = base_df['total_cost'].mean()
    res['ΔC'] = (res['C̄_base']-res['C̄'])/res['C̄_base']*100
    res['SL'] = 1-(dqn_df['stockout']>0).mean()
    res['SL_base'] = 1-(base_df['stockout']>0).mean()
    res['IT'] = demand.sum()/dqn_df['inv'].mean()
    res['IT_base'] = demand.sum()/base_df['inv'].mean()
    res['CVaR5'] = np.percentile(dqn_df['total_cost'],95)
    res['Gini'] = gini(dqn_df['total_cost'])
    return pd.Series(res)

def gini(x):
    x = np.sort(x)
    n = len(x)
    return (2*np.sum((np.arange(1,n+1)*x)))/(n*np.sum(x))-1

if __name__ == '__main__':
    print(' DQN-v7-Speed 训练开始（300 回合，指标稀疏化）...')
    os.makedirs('results', exist_ok=True)
    os.makedirs('tables', exist_ok=True)
    os.makedirs('charts', exist_ok=True)
    agent, metrics_df = train_v2_speed()
    print(' 评估与对比...')
    dqn_df = evaluate(agent)
    base_df = baseline()
    print(' 绘图（训练后统一）...')
    plot_all(dqn_df, base_df, metrics_df)
    print(' 导出表格...')
    export_tables(dqn_df, base_df, metrics_df)
    print(' 补货建议...')
    export_plan(agent)
    print(' 生成报告...')
    report(dqn_df, base_df, metrics_df)
    print(' 全部完成！结果目录：results/ tables/ charts/ + CSV + md 报告')

